package jpa4azure.impl;

import static jpa4azure.type.TypeWrapperFactory.$;
import static jpa4azure.util.TableHelper.getChildPartitionKeyColumn;
import static jpa4azure.util.TableHelper.getChildRowKeyColumn;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.metamodel.Metamodel;

import jpa4azure.type.CompoundFieldContext;
import jpa4azure.type.FieldContext;
import jpa4azure.type.Key;
import jpa4azure.type.TypeWrapper;
import jpa4azure.type.TypeWrapperFactory;
import jpa4azure.util.ResolverUtil;

import com.windowsazure.samples.Property;
import com.windowsazure.samples.PropertyCollection;
import com.windowsazure.samples.internal.authentication.DirectConnectToken;
import com.windowsazure.samples.table.AzureTableEntity;
import com.windowsazure.samples.table.AzureTableEntityCollection;
import com.windowsazure.samples.table.AzureTableManager;
import com.windowsazure.samples.table.Filter;
import com.windowsazure.samples.table.IllegalFilterOperandType;
import com.windowsazure.samples.table.TableOperationResponse;

public class AzureEntityManager implements EntityManager {

	// TableStorageClient client;
	AzureTableManager azureClient;
	DirectConnectToken token;

	private Map<Key, Object> cache;
	private Set<PersistanceCollection> relations;
	boolean isClosed = false;

	public AzureEntityManager(String storageAccountName,
			String storageAccountKey) {
		cache = new WeakHashMap<Key, Object>();
		relations = new HashSet<PersistanceCollection>();
		token = new DirectConnectToken(storageAccountName, storageAccountKey);
		try {
			azureClient = new AzureTableManager(token);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public void createTables(String... packages) {
		ResolverUtil<Object> resolver = new ResolverUtil<Object>();
		resolver.findAnnotated(Entity.class, packages);
		for (Class c : resolver.getClasses()) {
			TypeWrapper tw = TypeWrapperFactory.wrap(c);
			// client.createTableIfNotExist(tw.getTableName());
			azureClient.createTable(tw.getTableName(), new Date(), "jpa4azure",
					tw.getTableName());
		}
	}

	public AzureEntityManagerStats getStats() {
		return new AzureEntityManagerStats(cache.size(), relations.size());
	}

	public AzureTableManager getTableStorageClient() {
		return azureClient;
	}

	public void clear() {
		// TODO Auto-generated method stub

	}

	public void close() {
		flush();
		isClosed = true;
		azureClient = null;
	}

	public boolean contains(Object o) {
		TypeWrapper tw = TypeWrapperFactory.$(o);
		Key id = tw.id(o);
		return cache.containsKey(id);
	}

	public Query createNamedQuery(String arg0) {
		throw new UnsupportedOperationException();
	}

	public <T> TypedQuery<T> createNamedQuery(String arg0, Class<T> arg1) {
		throw new UnsupportedOperationException();
	}

	public Query createNativeQuery(String arg0) {
		throw new UnsupportedOperationException();
	}

	public Query createNativeQuery(String arg0, Class arg1) {
		throw new UnsupportedOperationException();
	}

	public Query createNativeQuery(String arg0, String arg1) {
		throw new UnsupportedOperationException();
	}

	public Query createQuery(String arg0) {
		throw new UnsupportedOperationException();
	}

	public <T> TypedQuery<T> createQuery(CriteriaQuery<T> arg0) {
		if (arg0 instanceof CriteriaQueryAdaptor) {
			return ((CriteriaQueryAdaptor<T>) arg0).getTypedQuery(this);
		} else
			return null;
	}

	public <T> TypedQuery<T> createQuery(final String query, final Class<T> c) {
		throw new UnsupportedOperationException();
	}

	public void detach(Object arg0) {
		throw new UnsupportedOperationException();
	}

	public <T> T find(Class<T> c, Object o) {
		T result;
		if (o instanceof Key)
			result = new FindOperation(this).findit(c, (Key) o);
		else
			result = new FindOperation(this).findit(c, o);
		return result;
	}

	public <T> T find(Class<T> arg0, Object arg1, Map<String, Object> arg2) {
		throw new UnsupportedOperationException();
	}

	public <T> T find(Class<T> arg0, Object arg1, LockModeType arg2) {
		throw new UnsupportedOperationException();
	}

	public <T> T find(Class<T> arg0, Object arg1, LockModeType arg2,
			Map<String, Object> arg3) {
		throw new UnsupportedOperationException();
	}

	public void flush() {
		for (Object o : cache.values()) {
			ObjectMeta meta = (ObjectMeta) ((ProxyObject) o).getHandler();
			if (meta.isDirty()) {
				try {
					new UpdateOperation(azureClient).save(o);
				} catch (Exception e) {
					throw new PersistenceException(e);
				}
				meta.reset();
			}
		}
		for (PersistanceCollection p : relations) {
			if (p.modified())
				p.save(this);
		}
	}

	public CriteriaBuilder getCriteriaBuilder() {
		throw new UnsupportedOperationException();
	}

	public Object getDelegate() {
		throw new UnsupportedOperationException();
	}

	public EntityManagerFactory getEntityManagerFactory() {
		throw new UnsupportedOperationException();
	}

	public FlushModeType getFlushMode() {
		throw new UnsupportedOperationException();
	}

	public LockModeType getLockMode(Object arg0) {
		throw new UnsupportedOperationException();
	}

	public Metamodel getMetamodel() {
		throw new UnsupportedOperationException();
	}

	public Map<String, Object> getProperties() {
		throw new UnsupportedOperationException();
	}

	public <T> T getReference(Class<T> arg0, Object arg1) {
		throw new UnsupportedOperationException();
	}

	public EntityTransaction getTransaction() {
		throw new UnsupportedOperationException();
	}

	public boolean isOpen() {
		throw new UnsupportedOperationException();
	}

	public void joinTransaction() {
		throw new UnsupportedOperationException();
	}

	public void lock(Object arg0, LockModeType arg1) {
		throw new UnsupportedOperationException();
	}

	public void lock(Object arg0, LockModeType arg1, Map<String, Object> arg2) {
		throw new UnsupportedOperationException();
	}

	public <T> T merge(T arg0) {
		throw new UnsupportedOperationException();
	}

	public void persist(Object entity) {
		TypeWrapper type = $(entity);
		try {
			initCollectionsRaw(entity, type);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		}
		try {
			new PersistOperation(new AzureTableManager(token)).save(entity);
		} catch (Exception e) {
			throw new PersistenceException(e);
		}
	}

	public void refresh(Object arg0) {
		throw new UnsupportedOperationException();
	}

	public void refresh(Object arg0, Map<String, Object> arg1) {
		throw new UnsupportedOperationException();
	}

	public void refresh(Object arg0, LockModeType arg1) {
		throw new UnsupportedOperationException();
	}

	public void refresh(Object arg0, LockModeType arg1, Map<String, Object> arg2) {
		throw new UnsupportedOperationException();
	}

	public void remove(Object o) {
		TypeWrapper type = $(o);
		String table = type.getTableName();
		Key k = type.id(o);
		for (Field f : type.getAggregateFields()) {
			for (AzureTableEntity rel : getRelationships(o, f.getName())) {
				azureClient.deleteEntity(table, rel.getPartitionKey(),
						rel.getRowKey(), null);
			}
		}

		TableOperationResponse response = azureClient.deleteEntity(table, k.getPartition(), k.getRow(), null);
		cache.remove(k);
	}

	public void setFlushMode(FlushModeType arg0) {
		throw new UnsupportedOperationException();
	}

	public void setProperty(String arg0, Object arg1) {
		throw new UnsupportedOperationException();
	}

	public <T> T unwrap(Class<T> arg0) {
		throw new UnsupportedOperationException();
	}

	public <T> Class<T> getProxy(Class<T> c) throws InstantiationException,
			IllegalAccessException {
		ProxyFactory f = new ProxyFactory();
		f.setInterfaces(new Class[] { Persistable.class });
		f.setSuperclass(c);
		return f.createClass();
	}

	public Object convert(AzureTableEntity e) {
		Map<String, Property> properties = new HashMap<String, Property>();
		for (Property p : e.getProperties()) {
			properties.put(p.getName(), p);
		}

		String typename = properties.get("jpa4azureBeanType")
				.getRepresentation();
		try {
			Class<?> c = getProxy(Class.forName(typename));
			Object o = c.newInstance();
			DefaultMethodHandler handler = new DefaultMethodHandler(this);
			((ProxyObject) o).setHandler(handler);
			TypeWrapper type = TypeWrapperFactory.$(o);
			initCollections(o, type);

			for (FieldContext field : type.getFields(o)) {
				String property = field.getFieldname();
				if (field instanceof CompoundFieldContext) {
					Property row = properties
							.get(getChildRowKeyColumn(property));
					Property partition = properties
							.get(getChildPartitionKeyColumn(property));
					if (row != null) {
						handler.add(property, partition.getRepresentation(),
								row.getRepresentation());
					}
				} else {
					Property p = properties.get(property);
					if (p!=null)
						field.setProperty(p.getValue());
				}
			}

			// apply the key value
			if (type.isHasKey())
				type.setId(o, e.getPartitionKey(), e.getRowKey());

			/*
			 * now activate the proxy so that it marks dirty state for saving on
			 * commit or flush
			 */
			cache.put(type.id(o), o);
			handler.setActive();
			return o;
		} catch (InstantiationException e1) {
			e1.printStackTrace();
		} catch (IllegalAccessException e1) {
			e1.printStackTrace();
		} catch (ClassNotFoundException e1) {
			e1.printStackTrace();
		}
		return null;
	}

	private void initCollections(Object target, TypeWrapper type)
			throws IllegalArgumentException, IllegalAccessException,
			InstantiationException {
		for (Field f : type.getAggregateFields()) {
			PersistanceCollection p = new PersistanceCollection(target, f, this);
			f.set(target, p);
			relations.add(p);
		}
	}

	private void initCollectionsRaw(Object target, TypeWrapper type)
			throws IllegalArgumentException, IllegalAccessException,
			InstantiationException {
		for (Field f : type.getAggregateFields()) {
			PersistanceCollection p = new PersistanceCollection(target, f, this);
			Collection c = (Collection) f.get(target);
			if (c != null)
				p.addAll(c);
			f.set(target, p);
			relations.add(p);
		}
	}

	public void relate(Object from, Object to, String property) {
		TypeWrapper fromType = $(from);
		TypeWrapper toType = $(to);
		Key fromKey = fromType.id(from);
		Key toKey = toType.id(to);
		String partitionKey = relationPartionKey(property, fromKey);
		String rowKey = toKey.getPartition() + toKey.getRow();
		String table = fromType.getTableName();
		PropertyCollection props = new PropertyCollection(partitionKey, rowKey);
		props.add(Property.newProperty("childPartitionKey",
				toKey.getPartition()));
		props.add(Property.newProperty("childRowKey", toKey.getRow()));
		azureClient.insertEntity("relation", new Date(), "jpa4azure", table,
				props);
	}

	private String relationPartionKey(String property, Key fromKey) {
		return property + fromKey.getPartition() + fromKey.getRow();
	}

	public AzureTableEntityCollection getRelationships(Object from,
			String property) {
		TypeWrapper fromType = $(from);
		String table = fromType.getTableName();
		Key fromKey = fromType.id(from);
		String partitionKey = relationPartionKey(property, fromKey);

		try {
			Filter f = Filter.Equal("PartitionKey", partitionKey);
			return azureClient.queryEntities(table, f, null);

		} catch (IllegalFilterOperandType e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	public Object loadByKey(String partition, String row, Class<?> c) {
		String table = TypeWrapperFactory.wrap(c).getTableName();
		return convert(azureClient.getEntity(table, partition, row));
	}

	public void removeRelationEntity(String partitionKey, String rowKey,
			Object from) {
		TypeWrapper fromType = $(from);
		String table = fromType.getTableName();
		TableOperationResponse res = azureClient.deleteEntity(table, partitionKey, rowKey, null);

	}

}
